package com.ncr.travel.appmgr.client.ui.table;

import java.util.ArrayList;
import java.util.List;

import com.google.gwt.user.client.ui.Widget;

/**
 * A simple model used to represent the contents of a set of tabular data. Each cell of the table is represented by a TableData object.
 * TabelModels can be used as the basis for LiveFlexTable instances
 * 
 * @author ma250087
 *
 * @param <E>
 */
public class TableModel<E extends TableData> {

    /**
     * This flag indicates whether column descriptor header is shown or not.
     *  default(true).
     */
    private boolean showColumnDescriptorHeader;
    private List<ColumnDescriptor> columns;
	private List<E> rows;
	private List<TableModelListener> listeners;

	/**
	 * Create a TableModel based on the ColumnDescriptions given
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model. 
	 *             The order of the elements in the array will be used for the order of the columns in the display
	 */
	public TableModel(ColumnDescriptor[] desc) {
		columns = new ArrayList<ColumnDescriptor>();
		for (ColumnDescriptor c : desc)
			columns.add(c);
		rows = new ArrayList<E>();
		listeners = new ArrayList<TableModelListener>();
		showColumnDescriptorHeader = true;
	}

	/**
	 * Create a TableModel based on the ColumnDescriptions given and fill the model with the given array of TableData objects
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model. 
	 *             The order of the elements in the array will be used for the order of the columns in the display
	 * @param data An array of objects who all implement the TableData interface.
	 *             These objects will be used to populate the model. 
	 *             The order of the elements defines the order the objects will be displayed in (each object represents a row of the table)
	 */
	public TableModel(ColumnDescriptor[] desc, E[] data) {
		this(desc);
		for (E row : data)
			rows.add(row);
	}

	/**
	 * Create a TableModel based on the ColumnDescriptions given and fill the model with the given List of TableData objects
	 * @param desc An array of ColumnDescriptions describing the properties of each column to be included in this model.
	 *             The order of the elements in the array will be used for the order of the columns in the display
	 * @param data A List of objects who all implement the TableData interface.
	 *             These objects will be used to populate the model.
	 *             The order of the elements defines the order the objects will be displayed in (each object represents a row of the table)
	 */
	public TableModel(ColumnDescriptor[] desc, List<E> data) {
		this(desc);
		rows = data;
	}

	/**
	 * Get the current row count
	 * @return The current row count
	 */
	public int getRowCount() {
		return rows.size();
	}

	/**
	 * Get the current column count
	 * @return The column count
	 */
	public int getColumnCount() {
		return columns.size();
	}

	/**
	 * Return the ith row of the table
	 * @param i The row offset to grab
	 * @return The ith row of the table
	 * @throws IndexOutOfBoundsException if the specified offset does not exist in the table
	 */
	public E getRow(int i) {
		return rows.get(i);
	}

	/**
	 * Return all of the rows in the table
	 * @return A List containing all of the rows in the table
	 */
	public List<E> getRows() {
		return rows;
	}

	/**
	 * Retrieve the Widget at the specified cell in the table
	 * @param row The row offset to access
	 * @param col The column offset to access
	 * @return The Widget stored at the given location in the table
	 * @throws IndexOutOfBoundsException if the specified cell does not exist in the table
	 */
	public Widget getCell(int row, int col) {
		return rows.get(row).get(columns.get(col).getId());
	}

	/**
	 * Store the specified Widget at the specified cell in the table
	 * @param w The Widget to store at the specified cell
	 * @param row The row offset to insert into
	 * @param col The column offset to insert into
	 * @throws IndexOutOfBoundsException if the specified cell does not exist in the table
	 */
	public void setCell(Widget w, int row, int col) {
		rows.get(row).set(columns.get(col).getId(), w);
		fireCellUpdate(row, col);
	}

	/**
	 * Add a new row to the table
	 * @param data The data to be added
	 * @param row The location offset to insert the row into; any rows following this one will be pushed down in the table
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void addRow(E data, int row) {
		validateRow(data);
		rows.add(row, data);
		fireRowUpdate(row);
	}

	/**
	 * Add a new row to the end of the table
	 * @param data The data to be added
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void addRow(E data) {
		validateRow(data);
		rows.add(data);
		fireRowUpdate(rows.size() - 1);
	}

	/**
	 * Replace a row with the given data
	 * @param data The replacement data
	 * @param row The row offset to replace in the table
	 * @throws IndexOutOfBoundsException if the given row does not exist in the table
	 * @throws IllegalArgumentException if the given data does not contain all of the columns used to describe the model at instantiation
	 */
	public void replaceRow(E data, int row) {
		validateRow(data);
		rows.remove(row);
		rows.add(row, data);
		fireRowUpdate(row);
	}

	/**
	 * Ensure each row added to the model contains all of the columns described at model instantiation
	 * @param data The data to be added
	 */
	private void validateRow(E data) {
		for (ColumnDescriptor c : columns)
			if (data.get(c.getId()) == null)
				throw new IllegalArgumentException("Argument does not contain required column data: " + c.getId());
	}

	/**
	 * Get an array of all the Widgets stored in the given column
	 * @param col The column offset to grab
	 * @return An array of all the Widgets stored in the given column, ordered by the current ordering of the model at the time of the call
	 * @throws IndexOutOfBoundsException if the given column does not exist in the table
	 */
	public Widget[] getColumn(int col) {
		List<Widget> list = new ArrayList<Widget>();
		for (int i = 0; i < rows.size(); ++i)
			list.add(getCell(i, col));
		return list.toArray(new Widget[0]);
	}

	/**
	 * Sort the model on the given column
	 * @param column The column offset to sort on
	 * @param ascending True if the sort should be ascending or false for a descending sort
	 * @return True if the sort completed successfully or false otherwise; may fail if the given column does not exist or the column was marked as not sortable in the ColumnDescriptor
	 */
	public boolean sort(int column, boolean ascending) {
		ColumnDescriptor desc = columns.get(column);
		if (desc == null || !desc.getIsSortable())
			return false;

		boolean order = ascending ? true : false;
		// TODO Bubble sort is evil and definitely won't scale to large data sets (but serves my current needs)!!
		boolean swapped;
		do {
			swapped = false;
			for (int i = 1; i < rows.size(); ++i) {
				if (rows.get(i - 1).compare(rows.get(i), columns.get(column).getId()) == order) {
					E data = rows.remove(i);
					rows.add(i - 1, data);
					swapped = true;
				}
			}
		} while (swapped);
		fireSortedEvent();
		return true;
	}

	/**
	 * Add a listener to this model
	 * @param l The listener object
	 */
	public void addListener(TableModelListener l) {
		listeners.add(l);
	}

	/**
	 * Return the column descriptor for the given column offset
	 * @param colOffset The column offset to retrieve information about
	 * @return The ColumnDescriptor for the given column offset
	 * @throws IndexOutOfBoundsException if the specified column offset does not exist
	 */
	public ColumnDescriptor getColumnDescriptor(int colOffset) {
		return columns.get(colOffset);
	}

	/**
	 * Remove the given listener from this model
	 * @param l The listener to remove
	 * @return True if the listener was removed or false otherwise; will return false if the given listener was not attached to this model
	 */
	public boolean removeListener(TableModelListener l) {
		return listeners.remove(l);
	}

	/**
	 * Remove all the listeners from this model
	 */
	public void removeAllListeners() {
		listeners.clear();
	}

	/**
	 * Fire the 'modelRowUpdated' event for the given row offset on this model
	 * @param row The row to be signaled as updated
	 */
	public void fireRowUpdate(int row) {
		for (TableModelListener l : listeners)
			l.modelRowUpdated(this, row);
	}

	/**
	 * Fire the 'modelCellUpdated' event for the given cell on this model
	 * @param row The row offset to be signaled as updated
	 * @param col The column offset to be signaled as updated
	 */
	public void fireCellUpdate(int row, int col) {
		for (TableModelListener l : listeners)
			l.modelCellUpdated(this, row, col);
	}

	/**
	 * Fire the 'modelSorted' event for this model
	 */
	protected void fireSortedEvent() {
		for (TableModelListener l : listeners)
			l.modelSorted(this);
	}
	
	 /**
     * @return the showColumnDescriptorHeader
     */
    public boolean isShowColumnDescriptorHeader() {
        return showColumnDescriptorHeader;
    }

    /**
     * @param showColumnDescriptorHeader the showColumnDescriptorHeader to set
     */
    public void setShowColumnDescriptorHeader(boolean showColumnDescriptorHeader) {
        this.showColumnDescriptorHeader = showColumnDescriptorHeader;
    }
    

	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((columns == null) ? 0 : columns.hashCode());
		result = prime * result + ((listeners == null) ? 0 : listeners.hashCode());
		result = prime * result + ((rows == null) ? 0 : rows.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		TableModel other = (TableModel) obj;
		if (columns == null) {
			if (other.columns != null)
				return false;
		} else if (!columns.equals(other.columns))
			return false;
		if (listeners == null) {
			if (other.listeners != null)
				return false;
		} else if (!listeners.equals(other.listeners))
			return false;
		if (rows == null) {
			if (other.rows != null)
				return false;
		} else if (!rows.equals(other.rows))
			return false;
		return true;
	}

}